package _7_lb

import (
	"errors"
	"hash/crc32"
	"math"
	"math/rand"
	"sort"
	"sync"
	"time"
)

type LoadBalancer interface {
	Select(nodes []*Node) (*Node, error)
}

type Node struct {
	Name      string
	Weight    int
	spotValue uint32
}

type RoundRobinLB struct {
	counter int
	mux     *sync.RWMutex
}

func (rr *RoundRobinLB) Select(nodes []*Node) (*Node, error) {
	rr.mux.Lock()
	defer rr.mux.Unlock()

	if len(nodes) == 0 {
		return nil, errors.New("empty nodes")
	}

	if len(nodes) == 1 {
		return nodes[0], nil
	}

	pos := rr.counter % len(nodes)
	rr.counter++

	return nodes[pos], nil
}

type WeightRandomLB struct {
	rd  rand.Rand
	mux *sync.RWMutex
}

func (wr *WeightRandomLB) Select(nodes []*Node) (*Node, error) {
	wr.mux.Lock()
	defer wr.mux.Unlock()
	total := 0
	for _, node := range nodes {
		total += node.Weight
	}

	rand.Seed(time.Now().UnixNano())
	target := int(rand.Int31n(int32(total)))
	for _, node := range nodes {
		if target < node.Weight {
			return node, nil
		}
		target -= node.Weight
	}
	return nil, errors.New("cant find a node")
}

const (
	defaultVirtualNodes = 400
)

type nodesArray []*Node

func (n nodesArray) Len() int {
	return len(n)
}

func (n nodesArray) Less(i, j int) bool {
	return n[i].spotValue < n[j].spotValue
}

func (n nodesArray) Swap(i, j int) {
	n[i], n[j] = n[j], n[i]
}

func (n nodesArray) Sort() {
	sort.Sort(n)
}

type ConsistentHashLB struct {
}

func (ch *ConsistentHashLB) Select(nodes []*Node) (*Node, error) {
	return nil, nil
}

type ConsistentHashSelector struct {
	virtualNodes int
	nodes        nodesArray
	weights      map[string]int
	mu           sync.RWMutex
}

func (chs *ConsistentHashSelector) Generate() {
	totalW := 0
	for _, w := range chs.weights {
		totalW += w
	}

	totalVirtualNodes := chs.virtualNodes * len(chs.weights)
	chs.nodes = nodesArray{}

	for nodeKey, w := range chs.weights {
		nodes := int(math.Floor(float64(w) / float64(totalW) * float64(totalVirtualNodes)))

		for i := 1; i < nodes; i++ {
			n := &Node{
				Name:      nodeKey,
				spotValue: uint32(xhash(nodeKey)),
			}

			chs.nodes = append(chs.nodes, n)
		}
	}
	chs.nodes.Sort()
}

func xhash(key string) int {
	v := int(crc32.ChecksumIEEE([]byte(key)))
	if v >= 0 {
		return v
	}
	return -v
}
